Skip to main content

Studies data Provider

Provider that calculates indicators data and pass the calculated data to the library.

/**
* Interface for receiving calculated studies data
*
* When calculating studies, parameters selected by user are transferred to this interface, then calculated studies are received from here
*
* When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.studies.DxStudiesDataProviderImplementation] and pass it to the library using [DxChartsDataProviders] data class
*
* The [dataFlow] property is used to get calculated studies data. It is a StateFlow that emits a list of [StudiesData] objects representing the studies data.
*
* The [setCandles] method is used to set candles data for studies calculation.
*
* The [setStudiesList] method is used to set a list of studies configurations that has to be calculated.
*
* The [getEnabledStudies] method is used to get a list of studies configurations that has to be calculated.
*
* The [updateTradingSessions] method is used to update trading sessions for studies calculators.
*/
interface DxChartsStudiesDataProvider {
/**
* Flow of receiving calculated studies data
*
* This property is a StateFlow that emits a list of [StudiesData] objects. Each [StudiesData] object represents the calculated studies data for a particular study.
*/
val dataFlow: StateFlow<List<StudiesData>>
/**
* Sets candles data for studies calculation
*
* This method is used to set the candles data that will be used for calculating the studies. The candles data is represented by a [DxChartsCandles] object.
*
* @param candles The candles data.
*/
fun setCandles(candles: DxChartsCandles)
/**
* Sets list of studies configurations that has to be calculated
*
* This method is used to set the list of studies configurations that will be calculated. Each configuration is represented by a [StudiesSetting] object.
*
* @param studies The list of studies configurations.
*/
fun setStudiesList(studies: List<StudiesSetting>)
/**
* Gets list of studies configurations that has to be calculated
*
* This method is used to get the list of studies configurations that are currently being calculated. Each configuration is represented by a [StudiesSetting] object.
*
* @return The list of studies configurations.
*/
fun getEnabledStudies(): List<StudiesSetting>
/**
* Updates trading sessions for studies calculators
*
* This method is used to update the trading sessions that will be used for calculating the studies. Each trading session is represented by a [TradingSession] object.
*
* @param sessions The list of trading sessions.
*/
fun updateTradingSessions(sessions: List<TradingSession>)
}

Interface methods:

  • setCandles: Sets candles data for calculations.
  • setStudiesList: Sets a list of indicators and their settings.
  • getEnabledStudies: Returns a list of indicators that are currently being calculated in the provider.
  • updateTradingSessions: Sets a trading sessions data for calculating indicators.

Data is sent by updating the state variable dataFlow. It is represented as a list of com.devexperts.dxcharts.provider.StudiesData objects.

Incoming data is represented as com.devexperts.dxcharts.provider.StudiesSetting objects.

/**
* Data class for storing calculated studies data
*
* @param uuid uuid of the data
* @param data DoubleArray with calculated data
*/
data class StudiesData(
val uuid: String = "",
val data: Array<DoubleArray?> = emptyArray()
)
/**
* Data class representing the settings for a study in a financial indicator.
*
* @property id The unique identifier for the study setting.
* @property title The title of the study.
* @property uuid The universally unique identifier (UUID) for the study.
* @property type The type of the indicator associated with the study.
* @property parameters The list of parameters for the study.
* @property lines The list of lines for the study.
* @property overlaying A boolean indicating whether the study is overlaying on the chart.
* @property calculateFutureData A boolean indicating whether to calculate future data for the study.
* @property categories A string representing the categories associated with the study.
* @property locked A nullable boolean indicating whether the study is locked.
*/
data class StudiesSetting(
val id: String,
val title: String,
val uuid: String,
val type: IndicatorType,
val parameters: List<Parameter>,
val lines: List<Line>,
val overlaying: Boolean,
val calculateFutureData: Boolean,
val categories: String,
val locked: Boolean?
) {
/**
* Data class representing a line associated with a study.
*
* @property title The title of the line.
* @property type The type of the study line.
* @property thickness The thickness of the line.
* @property colors The list of colors associated with the line.
* @property visible A boolean indicating whether the line is visible.
*/
data class Line(
val title: String?,
val type: Type?,
val thickness: Int?,
val colors: List<String>?,
val visible: Boolean?
) {
/**
* Enum class representing the types of study lines.
*/
enum class Type {
POINTS,
LINEAR,
HISTOGRAM,
DIFFERENCE,
ABOVE_CANDLE_TEXT,
TEXT,
BELOW_CANDLE_TEXT,
ABOVE_CANDLE_TRIANGLE,
TRIANGLE,
COLOR_CANDLE,
RECTANGULAR;
}
}
/**
* Data class representing a parameter associated with a study.
*
* @property id The unique identifier for the parameter.
* @property title The title of the parameter.
* @property type The type of the study parameter.
* @property value The value of the parameter.
* @property validation The [Validation] rules for the parameter.
* @property visible A boolean indicating whether the parameter is visible.
*/
data class Parameter(
val id: String,
val type: Type,
val value: Any?,
val validation: Validation?,
val visible: Boolean?
) {
/**
* Data class representing validation rules for a study parameter.
*
* @property min The minimum value allowed for the parameter.
* @property max The maximum value allowed for the parameter.
* @property precision The precision of the parameter value.
*/
data class Validation(
val min: Double?,
val max: Double?,
val precision: Int?,
)
/**
* Enum class representing the types of study parameters.
*/
enum class Type {
INTEGER_RANGE,
DOUBLE_RANGE,
PRICE_FIELD,
STRING,
AGGREGATION,
BOOLEAN,
AVERAGE,
UNDEFINED,
}
}
}
/**
* Data class representing a trading session with time range and optional high and low values.
*
* [TradingSession] needed for some indicators (e.g. Volume Weighted Average Price)
*
* @property from The starting time of the trading session.
* @property to The ending time of the trading session.
* @property high The highest value during the trading session (nullable).
* @property low The lowest value during the trading session (nullable).
*/
data class TradingSession(
val from: Double,
val to: Double,
val high: Double? = null,
val low: Double? = null
)

Calculated indicators looks as follows:

indicators

Here is the default implementation of DxChartsStudiesDataProvider:

/**
* Implementation of the [DxChartsStudiesDataProvider] interface, providing functionality
* to manage studies data and candles for DX charts.
*
* @property _dataFlow Internal mutable state flow to emit [StudiesData] to the UI.
* @property dataFlow Exposed immutable state flow to observe [StudiesData] from the UI.
* @property candlesState Internal mutable state flow to manage [DxChartsCandles].
* @property studiesListState Internal mutable state flow to manage the list of [StudiesSetting].
* @property sessionsFlow Internal mutable state flow to manage [TradingSession]s.
* @property executorService Executor service for managing background tasks.
* @property job Background job for executing data processing.
* @property dxStudies Volatile variable to store the [DxStudies] data.
* @property _errorFlow Internal [MutableStateFlow] for sending errors.
* @property errorFlow [StateFlow] for sending errors.
*/
class DxStudiesDataProviderImplementation : DxChartsStudiesDataProvider, DxChartsErrorProvider<StudiesProviderError> {
private val _dataFlow: MutableStateFlow<List<StudiesData>> =
MutableStateFlow(emptyList())
override val dataFlow: StateFlow<List<StudiesData>> get() = _dataFlow
private val candlesState: MutableStateFlow<DxChartsCandles> =
MutableStateFlow(DxChartsCandles())
private val studiesListState: MutableStateFlow<List<StudiesSetting>> =
MutableStateFlow(emptyList())
private val sessionsFlow: MutableStateFlow<List<TradingSession>> = MutableStateFlow(emptyList())
private val _errorFlow = MutableStateFlow<StudiesProviderError?>(null)
override val errorFlow: StateFlow<StudiesProviderError?> get() = _errorFlow
private var executorService: ExecutorService? = null
private var job: Job? = null
@Volatile
private var dxStudies: DxStudies<StudiesCandle> = DxStudies(0, emptyArray())
/**
* Starts the background data processing job.
* If a previous job exists, it is canceled before starting a new one.
* Inside this method, a background job is executed using the [executorService].
*
* When we get new candles, we update [dxStudies] using [updateDxStudies] function.
*
* When we get instrument we get trading sessions for it, that are necessary for some indicators.
We need to provide them to [dxStudies] with [DxStudies.setTradingSessions]
*
* After that we take list of parameters of enabled studies and use [DxStudies.createStudy] with this parameters
to create [Study]. Then we use [Study.calculateAll] to calculate studies data and create [StudiesData] with it.
*
* In the end we pass this calculated studies data to [_dataFlow]
*
* The data processing includes combining [DxChartsCandles], [StudiesSetting]s, and [TradingSession]s.
* Delta candles are calculated based on the difference between new and old candle data.
* Studies and trading session data are updated accordingly, and the result is emitted to the UI
* through the [_dataFlow] state flow.
*/
fun connect() {
if (job != null) {
job?.cancel()
job = null
}
if (executorService == null || executorService?.isShutdown == true) {
executorService = Executors.newFixedThreadPool(1)
executorService?.execute {
runBlocking {
job = launch(Dispatchers.IO) {
candlesState.combine(
studiesListState,
transform = { candles, studies -> Pair(candles, studies) }
).combine(
sessionsFlow,
transform = { pair, sessions -> Pair(pair, sessions) }
).collect { pair ->
val candles = pair.first.first
val settings = pair.first.second
val sessions = pair.second
try {
if (candles.candlesData.isEmpty()) {
updateDxStudies(0, listOf())
} else {
updateDxStudies(candles.candlesData.size, candles.candlesData)
}
if (candles.aggregation.isDayOrMore()) {
dxStudies.setTradingSessions(
candles.candlesData
.zipWithNext()
.map { (candle, nextCandle) ->
object : TradingSessionData {
override val from: Double =
candle.timestamp.toDouble()
override val high: Double? = null
override val low: Double? = null
override val to: Double =
nextCandle.timestamp.toDouble()
}
}.toTypedArray()
)
} else {dxStudies.setTradingSessions(
sessions.map { session ->
object : TradingSessionData {
override val from: Double = session.from
override val to: Double = session.to
override val high: Double? = session.high
override val low: Double? = session.low
}
}.toTypedArray()
)}
val studies = settings.map { studiesSetting ->
val params = getParams(studiesSetting).toTypedArray()
dxStudies.createStudy(studiesSetting.id, params)
}.mapIndexed { index, study ->
StudiesData(settings[index].uuid, study.calculateAll())
}
_dataFlow.emit(studies)
} catch (e: Exception) {
_errorFlow.tryEmit(StudiesProviderError.UnknownError("Failed to process studies data: $e.message", e))
}
}
}
}
}
} else {
Log.w(TAG, "ExecutorService is already running.")
}
}
/**
* Updates the [dxStudies] with the given size and candle data.
*
* @param size The size of the studies.
* @param candles The list of candle data [CandleDO].
*/
private fun updateDxStudies(size: Int, candles: List<CandleDO>) {
dxStudies = DxStudies(
size,
candles.map { it.toStudiesCandle() }.toTypedArray()
)
}
/**
* Converts studies configuration parameters to a list of [StudyParam].
*
* @param studiesConfig The studies configuration.
* @return List of [StudyParam] representing the configuration parameters.
*/
private fun getParams(studiesConfig: StudiesSetting): List<StudyParam> {
return studiesConfig.parameters.map { it.toStudyParam() }
}
/**
* Stops the background job and shuts down the executor service.
*/
fun disconnect() {
job?.cancel()
job = null
executorService?.shutdown()
executorService = null
}
/**
* Sets the DX chart candles for processing.
*
* @param candles The [DxChartsCandles].
*/
override fun setCandles(candles: DxChartsCandles) {
candlesState.tryEmit(candles)
}
/**
* Sets the list of studies settings for processing.
*
* @param studies The list of [StudiesSetting]s.
*/
override fun setStudiesList(studies: List<StudiesSetting>) {
studiesListState.tryEmit(studies)
}
/**
* Retrieves the list of studies settings of enabled indicators.
*
* @return List of [StudiesSetting].
*/
override fun getEnabledStudies(): List<StudiesSetting> {
return studiesListState.value
}
/**
* Updates the trading sessions for processing.
*
* @param sessions The list of [TradingSession]s.
*/
override fun updateTradingSessions(sessions: List<TradingSession>) {
sessionsFlow.tryEmit(sessions)
}
companion object{
private const val TAG = "DxStudiesDataProvider"
}
}
/**
* Sealed class representing different types of errors that might occur in the DxStudiesDataProviderImplementation.
*/
sealed class StudiesProviderError(override val message: String, override val error: Throwable?) : ProviderError {
class UnknownError(
override val message: String,
override val error: Throwable? = null
) : StudiesProviderError(message, error)
}